<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>11、Set和Map数据结构——Map</title>
	</head>
	<body>
		<div id="myDiv"></div>
		<script type="text/javascript">
			//三、Map
			//1.含义和基本用法
			//JavaScript 的对象（Object），本质上是键值对的集合（Hash 结构），但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
			const data = {};
			const element = document.getElementById('myDiv');
			data[element] = 'metadata';
			console.log(data['[object HTMLDivElement]']) //metadata
			//上面代码原意是将一个 DOM 节点作为对象data的键，但是由于对象只接受字符串作为键名，所以element被自动转为字符串[object HTMLDivElement]。
			//为了解决这个问题，ES6 提供了 Map 数据结构。它类似于对象，也是键值对的集合，但是“键”的范围不限于字符串，各种类型的值（包括对象）都可以当作键。也就是说，Object 结构提供了“字符串—值”的对应，Map结构提供了“值—值”的对应，是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构，Map 比 Object 更合适。
			const m = new Map();
			const o = {p:'Hello World!'};
			m.set(o,'content')
			console.log(m.get(o)) //content
			console.log(m.has(o))  //true
			console.log(m.delete(o)) //true
			console.log(m.has(o))   //false
			//上面代码使用Map结构的set方法，将对象o当做m的一个键，然后又使用get方法读取这个键，接着使用delete方法删除了这个键。
			//上面的例子展示了如何向 Map 添加成员。作为构造函数，Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
			const map = new Map([
				['name','张三'],
				['title','Author']
			]);
			console.log(map.size)  //2
			console.log(map.has('name'))  //true
			console.log(map.get('name'))  //张三
			console.log(map.has('title'))  //true
			console.log(map.get('title')) //Author
			//上面代码在新建 Map 实例时，就指定了两个键name和title。
			
			//Map构造函数接受数组作为参数，实际上执行的是下面的算法。
			const items = [
				['name','张三'],
				['title','Author']
			];
			const map_1 = new Map();
			items.forEach(
				([key,value]) => map_1.set(key,value)
			)
			//事实上，不仅仅是数组，任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构（详见《Iterator》一章）都可以当作Map构造函数的参数。这就是说，Set和Map都可以用来生成新的 Map。
			const set = new Set([
				['foo',1],
				['bar',2]
			]);
			const m1 = new Map(set);
			console.log(m1.get('foo'))  //1
			
			const m2 = new Map([['baz',3]]);
			const m3 = new Map(m2);
			console.log(m3.get('baz')) //3
			//上面代码中，我们分别使用 Set 对象和 Map 对象，当作Map构造函数的参数，结果都生成了新的 Map 对象。
			
			//如果对同一个键多次赋值，后面的值将覆盖前面的值。
			const map_2 = new Map();
			map_2.set(1,'aaa')
				 .set(1,'bbb')
			console.log(map_2.get(1))  //bbb
			//上面代码对键1连续赋值两次，后一次的值覆盖前一次的值。
			//如果读取一个未知的键，则返回undefined。
			console.log(new Map().get('asfddfajknk'))  //undefined
			//注意，只有对同一个对象的引用，Map 结构才将其视为同一个键。这一点要非常小心。
			const map_3 = new Map();
			map.set(['a'],555)
			console.log(map.get(['a']))  //undefined
			//上面代码的set和get方法，表面是针对同一个键，但实际上这是两个值，内存地址是不一样的，因此get方法无法读取该键，返回undefined。
			//同理，同样的值的两个实例，在 Map 结构中被视为两个键。
			const map_4 = new Map();
			const k1 = ['a'];
			const k2 = ['a'];
			map_4.set(k1,111)
			     .set(k2,222)
			map_4.get(k1) //111
			map_4.get(k2) //222
			//上面代码中，变量k1和k2的值是一样的，但是它们在 Map 结构中被视为两个键。
			//由上可知，Map 的键实际上是跟内存地址绑定的，只要内存地址不一样，就视为两个键。这就解决了同名属性碰撞（clash）的问题，我们扩展别人的库的时候，如果使用对象作为键名，就不用担心自己的属性与原作者的属性同名。
			//如果 Map 的键是一个简单类型的值（数字、字符串、布尔值），则只要两个值严格相等，Map 将其视为一个键，比如0和-0就是一个键，布尔值true和字符串true则是两个不同的键。另外，undefined和null也是两个不同的键。虽然NaN不严格相等于自身，但 Map 将其视为同一个键。
			let map_5 = new Map();
			map_5.set(-0,123);
			console.log(map_5.get(+0))  //123
			
			map_5.set(true,1);
			map_5.set('true',2);
			console.log(map_5.get(true)) //1
			
			map_5.set(undefined,3);
			map_5.set(null,4);
			console.log(map_5.get(undefined))  //3
			
			map_5.set(NaN,123);
			console.log(map_5.get(NaN))  //123
			
			
			//2.实例的属性和操作方法
			//Map结构的实例有以下属性和操作方法。
			//（1）、size属性
			//size属性返回Map结构的成员总数
			const map_6 = new Map();
			map_6.set('foo',true);
			map_6.set('bar',false)
			console.log('map_6.size:'+map_6.size) //map_6.size:2
			
			//（2）、set(key,value)
			//set方法设置键名key对应的键值为value，然后返回整个 Map 结构。如果key已经有值，则键值会被更新，否则就新生成该键。
			const m_2 = new Map();
			m_2.set('edition',6);  //键是字符串
			m_2.set(262,'standrd');  //键是数值
			m_2.set(undefined,'nah');  //键是undefined
			//set方法返回的是当前的Map对象，因此可以采用链式写法。
			let map_7 = new Map()
				.set(1,'a')
				.set(2,'b')
				.set(3,'c')
				
			//（3）、get(key)
			//get方法读取key对应的键值，如果找不到key，返回undefined。
			const m_3 = new Map();
			const hello = function(){
				console.log('hello')
			}
			m.set(hello,'Hello ES6') //键是函数
			console.log(m.get(hello)) //Hello ES6
			
			//(4)、has(key)
			//has方法返回一个布尔值，表示某个键是否在当前Map对象之中。
			const m_4 = new Map();
			m_4.set('edition',6);  //键是字符串
			m_4.set(262,'standrd');  //键是数值
			m_4.set(undefined,'nah');  //键是undefined
			
			console.log(m_4.has('edition')) //true
			console.log(m_4.has('years'))  //false
			console.log(m_4.has(262))      //true
			console.log(m_4.has(undefined)) //true
			
			//（5）、delete(key)
			//delete方法删除某个键，返回true。如果删除失败，返回false。
			const m_5 = new Map();
			m_5.set(undefined,'nah');
			console.log(m_5.has(undefined))  //true
			m_5.delete(undefined)
			console.log(m_5.has(undefined))  //false
			
			//（6）、clear()
			//clear方法清除所有成员，没有返回值。
			let map_8 = new Map();
			map_8.set('foo',true);
			map_8.set('bar',false);
			console.log(map_8.size)  //2
			map_8.clear();
			console.log(map_8.size)  //0
			
			
			//3.遍历方法
			//Map 结构原生提供三个遍历器生成函数和一个遍历方法。
			
		    //keys()：返回键名的遍历器。
		    //values()：返回键值的遍历器。
		    //entries()：返回所有成员的遍历器。
		    //forEach()：遍历 Map 的所有成员。

			//需要特别注意的是，Map 的遍历顺序就是插入顺序。
			const map_9 = new Map([
				['F','no'],
				['T','yes']
			])
			for(let key of map_9.keys()){
				console.log(key)
			}
			// F
			// T
			
			for(let value of map_9.values()){
				console.log(value);
			}
			// no
			// yes
			
			for(let item of map_9.entries()){
				console.log(item[0],item[1])
			}
			// F  no
			// T  yes
			
			//或者
			for(let [key,value] of map_9.entries()){
				console.log(key,value)
			}
			// F  no
			// T  yes
			
			//等同于使用map.entries()
			for(let [key,value] of map_9){
				console.log(key,value)
			}
			// F  no
			// T  yes
			
			//上面代码最后的那个例子，表示 Map 结构的默认遍历器接口（Symbol.iterator属性），就是entries方法。
			console.log(map[Symbol.iterator]===map.entries)  //true
			
			//Map 结构转为数组结构，比较快速的方法是使用扩展运算符（...）
			const map_11= new Map([
				[1,'one'],
				[2,'two'],
				[3,'three']
			])
			console.log([...map_11.keys()]) //[1, 2, 3]
			console.log([...map_11.values()])  //["one", "two", "three"]
			console.log([...map_11.entries()]) //[[1,'one'],[2,'two'],[3,'three']]
			console.log([...map]) //[[1,'one'],[2,'two'],[3,'three']]
			//结合数组的map方法、filter方法，可以实现 Map 的遍历和过滤（Map 本身没有map和filter方法）。
			const map_12 = new Map()
				.set(1,'a')
				.set(2,'b')
				.set(3,'c')
			const map_13 = new Map(
				[...map_12].filter(([k,v]) => k<3)
			);
			//// 产生 Map 结构 {1 => 'a', 2 => 'b'}
			const map_14 = new Map(
				[...map_12].map(([k,v]) => [k*2,'_'+v])
			);
			//// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
			//此外，Map 还有一个forEach方法，与数组的forEach方法类似，也可以实现遍历。
			map_13.forEach(function(value,key,map){
				console.log('key:%s,value:%s',key,value)
			})
			//key:1,value:a
			//key:2,value:b
			//forEach方法还可以接受第二个参数，用来绑定this。
			const reporter = {
				report:function(key,value){
					console.log('key:%s,value:%s',key,value);
				}
			}
			map_13.forEach(function(value,key,map){
				this.report(key,value);
			},reporter);
			//key:1,value:a
			//key:2,value:b
			//上面代码中，forEach方法的回调函数的this，就指向reporter。
			
			
			//3.与其他数据结构的互相转换
			//（1）、Map转为数组
			//前面已经提过，Map 转为数组最方便的方法，就是使用扩展运算符（...）。
			const myMap = new Map()
				.set(true,7)
				.set({foo:3},['abc']);
			console.log([...myMap])// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
			
			//（2）、数组转为Map
			//将数组传入Map构造函数，就可以转为Map
			var map_15 = new Map([
				[true,5],
				[{bar:2},['abc']]
			])
			console.log(map_15) //Map(2) {true => 5, Object {bar: 2} => ["abc"]}
			
			//（3）、Map转为对象
			//如果所有 Map 的键都是字符串，它可以转为对象。
			function strMapToObj(strMap){
				let obj = Object.create(null);
				for(let [k,v] of strMap){
					obj[k] = v;
				}
				return obj;
			}
			const myMap_1 = new Map()
				.set('yes',true)
				.set('no',false);
			console.log(strMapToObj(myMap_1)) //Object {yes: true, no: false}
			
			//（4）、对象转为Map
			function objToStrMap(obj){
				let strMap = new Map();
				for(let k of Object.keys(obj)){
					strMap.set(k,obj[k])
				}
				return strMap;
			}
			console.log(objToStrMap({'yes':true,'no':false})) //Map(2) {"yes" => true, "no" => false}
			
			//（5）、Map转为JSON
			//Map 转为 JSON 要区分两种情况。一种情况是，Map 的键名都是字符串，这时可以选择转为对象 JSON。
			function strMapToJson(strMap){
				return JSON.stringify(strMapToObj(strMap))
			}
			let myMap_2 = new Map().set('yes',true).set('no',false);
			console.log(strMapToJson(myMap_2)) //{"yes":true,"no":false}
			//另一种情况是，Map 的键名有非字符串，这时可以选择转为数组 JSON。
			function mapToArrayJson(map){
				return JSON.stringify([...map]);
			}
			let myMap_3 = new Map().set(true,7).set({foo:3},['abc'])
			console.log(mapToArrayJson(myMap)) //[[true,7],[{"foo":3},["abc"]]]
			
			//（6）、JSON转为Map
			//JSON 转为 Map，正常情况下，所有键名都是字符串。
			function jsonToStrMap(jsonStr){
				return objToStrMap(JSON.parse(jsonStr));
			}
			console.log(jsonToStrMap('{"yes":true,"no":false}'))
			//Map(2) {"yes" => true, "no" => false}
			
			//但是，有一种特殊情况，整个 JSON 就是一个数组，且每个数组成员本身，又是一个有两个成员的数组。这时，它可以一一对应地转为Map。这往往是数组转为 JSON 的逆操作。
			function jsonToMap(jsonStr) {
				return new Map(JSON.parse(jsonStr));
			}

			console.log(jsonToMap('[[true,7],[{"foo":3},["abc"]]]'))
			//Map(2) {true => 7, Object {foo: 3} => ["abc"]}
		</script>
	</body>
</html>
